package com.liuche.homechat.common.websocket.service.impl;

import cn.hutool.core.util.RandomUtil;
import cn.hutool.json.JSONUtil;
import com.github.benmanes.caffeine.cache.Cache;
import com.github.benmanes.caffeine.cache.Caffeine;
import com.liuche.homechat.common.common.enums.YNEnum;
import com.liuche.homechat.common.common.event.UserOnlineEvent;
import com.liuche.homechat.common.common.utils.IpUtil;
import com.liuche.homechat.common.common.utils.NettyUtil;
import com.liuche.homechat.common.user.dao.UserDao;
import com.liuche.homechat.common.user.domain.entity.IpInfo;
import com.liuche.homechat.common.user.domain.entity.User;
import com.liuche.homechat.common.user.service.LoginService;
import com.liuche.homechat.common.websocket.constant.WXConstant;
import com.liuche.homechat.common.websocket.domaiin.dto.WSAuthorize;
import com.liuche.homechat.common.websocket.domaiin.dto.WebSocketConnectDTO;
import com.liuche.homechat.common.websocket.domaiin.vo.resp.WSBaseResp;
import com.liuche.homechat.common.websocket.domaiin.vo.resp.WSLoginSuccess;
import com.liuche.homechat.common.websocket.domaiin.vo.resp.WSLoginUrl;
import com.liuche.homechat.common.websocket.service.CustomWebSocketService;
import com.liuche.homechat.common.websocket.service.adapter.WebSocketAdapter;
import io.netty.channel.Channel;
import io.netty.channel.ChannelHandlerContext;
import io.netty.handler.codec.http.websocketx.TextWebSocketFrame;
import io.netty.handler.ssl.CipherSuiteConverter;
import lombok.SneakyThrows;
import lombok.extern.slf4j.Slf4j;
import me.chanjar.weixin.mp.api.WxMpService;
import me.chanjar.weixin.mp.bean.result.WxMpQrCodeTicket;
import org.apache.commons.lang3.ObjectUtils;
import org.springframework.context.ApplicationEventPublisher;
import org.springframework.stereotype.Service;

import javax.annotation.Resource;
import java.time.Duration;
import java.time.LocalDateTime;
import java.util.Date;
import java.util.Objects;
import java.util.concurrent.ConcurrentHashMap;

/**
 * @author：liuche
 * @Date：2024年05月26日 19:40
 * @Description：
 */
@Service("WSService")
@Slf4j
public class CustomWebSocketServiceImpl implements CustomWebSocketService {
    /**
     * 管理未登录/已登录的用户
     */
    private static final ConcurrentHashMap<Channel, WebSocketConnectDTO> WS_CONNECT_MAP = new ConcurrentHashMap<>(100);

    /**
     * 管理执行认证的用户
     *
     * @param channel 通道
     */

    // 缓存时间
    private static final Duration DURATION = Duration.ofHours(1L);

    // 最大值
    private static final int MAXIMUN_SIZE = 10000;

    /**
     * 临时保留登录 code 和 Channel 的映射
     */
    private static final Cache<Integer, Channel> WAIT_LOGIN_MAP = Caffeine.newBuilder()
            .maximumSize(MAXIMUN_SIZE)
            .expireAfterWrite(DURATION)
            .build();

    @Resource
    private WxMpService wxMpService;

    @Resource
    private UserDao userDao;

    @Resource
    private LoginService loginService;

    @Resource
    private ApplicationEventPublisher applicationEventPublisher;


    @Override
    public void connectActive(Channel channel) {
        log.info("新建立了连接：{}", channel);
        WS_CONNECT_MAP.put(channel, new WebSocketConnectDTO());
    }

    @Override
    @SneakyThrows
    public void loginAuthorize(Channel channel) {
        Integer code = generateCode(channel);

        // 得到微信的带参二维码，并响应到前端
        WxMpQrCodeTicket codeTicket = wxMpService.getQrcodeService().qrCodeCreateTmpTicket(code, WXConstant.WX_CODE_EXPIRESECONDS);
        // 组装好返回信息
        WSBaseResp<WSLoginUrl> resp = WebSocketAdapter.getResp(codeTicket);
        sendMsg(channel, resp);
    }

    @Override
    public void unConnect(ChannelHandlerContext ctx) {
        WS_CONNECT_MAP.remove(ctx.channel());
        log.info("[{}]断开了连接", ctx.channel());
        ctx.channel().close(); // 关闭连接
    }

    @Override
    public void authorize(Long uid, String code) {
        // 得到用户信息
        User user = userDao.getById(uid);

        // 生成 token 返回给客户端
        String token = loginService.login(uid);

        // 找到关联的渠道
        Channel channel = WAIT_LOGIN_MAP.asMap().get(Integer.valueOf(code));
        if (ObjectUtils.isEmpty(channel)) {
            return;
        }

        // 登录成功
        loginSuccess(channel, user, token);
    }

    @Override
    public void promptUserAuthorize(String code) {
        Channel channel = WAIT_LOGIN_MAP.asMap().get(Integer.valueOf(code));
        WSBaseResp<String> baseResp = WebSocketAdapter.getPromptUserAuthorizeResp();

        sendMsg(channel, baseResp);
    }

    @Override
    public void handShakeAuthorize(Channel channel, WSAuthorize wsAuthorize) {
        String token = wsAuthorize.getToken();
        boolean verify = loginService.verify(token);
        if (verify) {
            // 认证成功
            User user = userDao.getById(loginService.getValidUid(wsAuthorize.getToken()));
            loginSuccess(channel, user, wsAuthorize.getToken());
        } else {
            // 认证失败
            sendMsg(channel, WebSocketAdapter.buildInvalidateTokenResp());
        }

    }

    @Override
    public void noticeOnlineUsers(User user) {
        for (Channel channel : WS_CONNECT_MAP.keySet()) {
            WSBaseResp<Long> resp = WebSocketAdapter.getUserOnlineNoticeInfo(user);
            sendMsg(channel, resp);
        }
    }

    private void loginSuccess(Channel channel, User user, String token) {
        // 返回给前端认证通过消息
        WSBaseResp<WSLoginSuccess> resp = WebSocketAdapter.getLoginResp(user, token);
        // 当前渠道关联对应的 uid
        WS_CONNECT_MAP.put(channel, WebSocketConnectDTO.builder()
                .uid(user.getId())
                .build());

        // 得到 ip 信息，并发送用户上线事件
        String ip = NettyUtil.getAttr(channel, NettyUtil.IP);
        IpInfo ipInfo = IpUtil.buildIpInfo(user, ip);
        user.setIpInfo(ipInfo);
        // 更新状态信息
        user.setLastOptTime(LocalDateTime.now());
        user.setActiveStatus(YNEnum.Y.getStatus());
        applicationEventPublisher.publishEvent(new UserOnlineEvent(this, user));

        sendMsg(channel, resp);
    }


    private Integer generateCode(Channel channel) {
        Integer code;
        do {
            code = RandomUtil.randomInt(10000, 99999);
        } while (Objects.nonNull(WAIT_LOGIN_MAP.asMap().putIfAbsent(code, channel)));

        return code;
    }


    /**
     * 发送消息到前端
     * @param channel
     * @param wsBaseResp
     * @param <T>
     */
    private <T> void sendMsg(Channel channel, WSBaseResp<T> wsBaseResp) {
        channel.writeAndFlush(new TextWebSocketFrame(JSONUtil.toJsonStr(wsBaseResp)));
    }
}
